/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 *    Option.java
 *    Copyright (C) 1999-2012 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.core;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;

/**
 * Class to store information about an option.
 * <p>
 *
 * Typical usage:
 * <p>
 *
 * <code>Option myOption = new Option("Uses extended mode.", "E", 0, "-E")); </code>
 * <p>
 *
 * @author Eibe Frank (eibe@cs.waikato.ac.nz)
 * @version $Revision$
 */
public class Option {

    /** A cache of property descriptors */
    private static final Map<Class<?>, PropertyDescriptor[]> s_descriptorCache = new HashMap<Class<?>, PropertyDescriptor[]>();

    /** What does this option do? */
    private String m_Description;

    /** The synopsis. */
    private String m_Synopsis;

    /** What's the option's name? */
    private String m_Name;

    /** How many arguments does it take? */
    private int m_NumArguments;

    /**
     * Creates new option with the given parameters.
     *
     * @param description  the option's description
     * @param name         the option's name
     * @param numArguments the number of arguments
     * @param synopsis     the option's synopsis
     */
    public Option(String description, String name, int numArguments, String synopsis) {

        m_Description = description;
        m_Name = name;
        m_NumArguments = numArguments;
        m_Synopsis = synopsis;
    }

    /**
     * Get a list of options for a class. Options identified by this method are bean
     * properties (with get/set methods) annotated using the OptionMetadata
     * annotation. All options from the class up to, but not including, the supplied
     * oldest superclass are returned.
     * 
     * 
     * @param childClazz          the class to get options for
     * @param oldestAncestorClazz the oldest superclass (inclusive) at which to stop
     *                            getting options from
     * @return a list of options
     */
    public static Vector<Option> listOptionsForClassHierarchy(Class<?> childClazz, Class<?> oldestAncestorClazz) {
        Vector<Option> results = listOptionsForClass(childClazz);

        Class<?> parent = childClazz;
        do {
            parent = parent.getSuperclass();
            if (parent == null) {
                break;
            }
            results.addAll(listOptionsForClass(parent));
        } while (!parent.equals(oldestAncestorClazz));

        return results;
    }

    /**
     * Adds all methods from the supplied class to the supplied list of methods.
     * 
     * @param clazz    the class to get methods from
     * @param methList the list to add them to
     */
    protected static void addMethodsToList(Class<?> clazz, List<Method> methList) {
        Method[] methods = clazz.getDeclaredMethods();
        for (Method m : methods) {
            methList.add(m);
        }
    }

    /**
     * Gets a list of options for the supplied class. Only examines immediate
     * methods in the class (does not consider superclasses). Options identified by
     * this method are bean properties (with get/set methods) annotated using the
     * OptionMetadata annotation.
     * 
     * @param clazz the class to examine for options
     * @return a list of options
     */
    public static Vector<Option> listOptionsForClass(Class<?> clazz) {
        Vector<Option> results = new Vector<Option>();
        List<Method> allMethods = new ArrayList<Method>();
        addMethodsToList(clazz, allMethods);

        Class<?>[] interfaces = clazz.getInterfaces();
        for (Class c : interfaces) {
            addMethodsToList(c, allMethods);
        }

        Option[] unsorted = new Option[allMethods.size()];
        int[] opOrder = new int[allMethods.size()];
        for (int i = 0; i < opOrder.length; i++) {
            opOrder[i] = Integer.MAX_VALUE;
        }
        int index = 0;
        for (Method m : allMethods) {
            OptionMetadata o = m.getAnnotation(OptionMetadata.class);
            if (o != null) {
                if (o.commandLineParamName().length() > 0) {
                    opOrder[index] = o.displayOrder();
                    String description = o.description();
                    if (!description.startsWith("\t")) {
                        description = "\t" + description;
                    }
                    description = description.replace("\n", "\n\t");
                    String name = o.commandLineParamName();
                    if (name.startsWith("-")) {
                        name = name.substring(1, name.length());
                    }
                    String synopsis = o.commandLineParamSynopsis();
                    if (!synopsis.startsWith("-")) {
                        synopsis = "-" + synopsis;
                    }
                    int numParams = o.commandLineParamIsFlag() ? 0 : 1;
                    Option option = new Option(description, name, numParams, synopsis);
                    unsorted[index] = option;
                    index++;
                }
            }
        }

        int[] sortedOpts = Utils.sort(opOrder);
        for (int i = 0; i < opOrder.length; i++) {
            if (opOrder[i] < Integer.MAX_VALUE) {
                results.add(unsorted[sortedOpts[i]]);
            }
        }

        return results;
    }

    /**
     * Get the settings of the supplied object. Settings identified by this method
     * are bean properties (with get/set methods) annotated using the OptionMetadata
     * annotation. All options from the class up to, but not including, the supplied
     * oldest superclass are returned.
     * 
     * @param target              the target object to get settings for
     * @param oldestAncestorClazz the oldest superclass at which to stop getting
     *                            options from
     * @return
     */
    public static String[] getOptionsForHierarchy(Object target, Class<?> oldestAncestorClazz) {

        ArrayList<String> options = new ArrayList<String>();
        for (String s : getOptions(target, target.getClass())) {
            options.add(s);
        }

        Class<?> parent = target.getClass();
        do {
            parent = parent.getSuperclass();
            if (parent == null) {
                break;
            }
            for (String s : getOptions(target, parent)) {
                options.add(s);
            }
        } while (!parent.equals(oldestAncestorClazz));

        return options.toArray(new String[options.size()]);
    }

    /**
     * Get the settings of the supplied object. Settings identified by this method
     * are bean properties (with get/set methods) annotated using the OptionMetadata
     * annotation. Options belonging to the targetClazz (either the class of the
     * target or one of its superclasses) are returned.
     * 
     * @param target      the target to extract settings from
     * @param targetClazz the class to consider for obtaining settings - i.e.
     *                    annotated methods from this class will have their values
     *                    extracted. This class is expected to be either the class
     *                    of the target or one of its superclasses
     * @return an array of settings
     */
    public static String[] getOptions(Object target, Class<?> targetClazz) {

        ArrayList<String> options = new ArrayList<String>();
        try {
            Object[] args = {};
            Class<?> parent = targetClazz.getSuperclass();
            PropertyDescriptor[] properties = getPropertyDescriptors(targetClazz, parent);

            for (PropertyDescriptor p : properties) {
                Method getter = p.getReadMethod();
                Method setter = p.getWriteMethod();
                if (getter == null || setter == null) {
                    continue;
                }
                OptionMetadata parameterDescription = null;
                parameterDescription = getter.getAnnotation(OptionMetadata.class);
                if (parameterDescription == null) {
                    parameterDescription = setter.getAnnotation(OptionMetadata.class);
                }

                if (parameterDescription != null && parameterDescription.commandLineParamName().length() > 0) {
                    Object value = getter.invoke(target, args);
                    if (value != null) {
                        if (!parameterDescription.commandLineParamIsFlag()) {
                            options.add("-" + parameterDescription.commandLineParamName());
                        }

                        if (value.getClass().isArray()) {
                            if (parameterDescription.commandLineParamIsFlag()) {
                                throw new IllegalArgumentException("Getter method for a command " + "line flag should return a boolean value");
                            }
                            if (((Object[]) value).length == 0) {
                                // remove the initial option identifier (added above)
                                // as the array is empty
                                options.remove(options.size() - 1);
                            }
                            int index = 0;
                            for (Object element : (Object[]) value) {
                                if (index > 0) {
                                    options.add("-" + parameterDescription.commandLineParamName());
                                }
                                if (element instanceof OptionHandler) {
                                    options.add(getOptionStringForOptionHandler((OptionHandler) element));
                                } else {
                                    options.add(element.toString());
                                }
                                index++;
                            }
                        } else if (value instanceof OptionHandler) {
                            if (parameterDescription.commandLineParamIsFlag()) {
                                throw new IllegalArgumentException("Getter method for a command " + "line flag should return a boolean value");
                            }
                            options.add(getOptionStringForOptionHandler((OptionHandler) value));
                        } else if (value instanceof SelectedTag) {
                            options.add("" + ((SelectedTag) value).getSelectedTag().getReadable());
                        } else {
                            // check for boolean/flag
                            if (parameterDescription.commandLineParamIsFlag()) {
                                if (!(value instanceof Boolean)) {
                                    throw new IllegalArgumentException("Getter method for a command " + "line flag should return a boolean value");
                                }
                                if (((Boolean) value).booleanValue()) {
                                    options.add("-" + parameterDescription.commandLineParamName());
                                }
                            } else {
                                if (value.toString().length() > 0) {
                                    options.add(value.toString());
                                } else {
                                    // don't allow empty strings
                                    options.remove(options.size() - 1);
                                }
                            }
                        }
                    }
                }
            }

        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return options.toArray(new String[options.size()]);
    }

    /**
     * Construct a String containing the class name of an OptionHandler and its
     * option settings
     *
     * @param handler the OptionHandler to construct an option string for
     * @return a String containing the name of the handler class and its options
     */
    protected static String getOptionStringForOptionHandler(OptionHandler handler) {
        String optHandlerClassName = handler.getClass().getCanonicalName();
        String optsVal = Utils.joinOptions(handler.getOptions());
        String totalOptVal = optHandlerClassName + " " + optsVal;

        return totalOptVal;
    }

    /**
     * Sets options on the target object. Settings identified by this method are
     * bean properties (with get/set methods) annotated using the OptionMetadata
     * annotation. All options from the class up to, but not including, the supplied
     * oldest superclass are processed in order.
     *
     * @param options             the options to set
     * @param target              the target on which to set options
     * @param oldestAncestorClazz the oldest superclass at which to stop setting
     *                            options
     */
    public static void setOptionsForHierarchy(String[] options, Object target, Class<?> oldestAncestorClazz) {

        setOptions(options, target, target.getClass());

        Class<?> parent = target.getClass();
        do {
            parent = parent.getSuperclass();
            if (parent == null) {
                break;
            }

            setOptions(options, target, parent);
        } while (!parent.equals(oldestAncestorClazz));
    }

    /**
     * Get property descriptors for a target class. Checks a cache first before
     * using introspection.
     *
     * @param targetClazz the target to get the descriptors for
     * @param parent      the parent class at which to stop getting descriptors
     * @return an array of property descriptors
     * @throws IntrospectionException if a problem occurs
     */
    private static PropertyDescriptor[] getPropertyDescriptors(Class<?> targetClazz, Class<?> parent) throws IntrospectionException {

        PropertyDescriptor[] result = s_descriptorCache.get(targetClazz);
        if (result == null) {
            BeanInfo bi = Introspector.getBeanInfo(targetClazz, parent);
            result = bi.getPropertyDescriptors();
            s_descriptorCache.put(targetClazz, result);
        }

        return result;
    }

    /**
     * Sets options on the target object. Settings identified by this method are
     * bean properties (with get/set methods) annotated using the OptionMetadata
     * annotation. Options from just the supplied targetClazz (which is expected to
     * be either the class of the target or one of its superclasses) are set.
     *
     * @param options     the options to set
     * @param target      the target on which to set options
     * @param targetClazz the class containing options to be be set - i.e. annotated
     *                    option methods in this class will have their values set.
     *                    This class is expected to be either the class of the
     *                    target or one of its superclasses
     */
    public static void setOptions(String[] options, Object target, Class<?> targetClazz) {
        if (options != null && options.length > 0) {
            // Set the options just for this class
            try {
                Object[] getterArgs = {};
                Class<?> parent = targetClazz.getSuperclass();
                PropertyDescriptor[] properties = getPropertyDescriptors(targetClazz, parent);

                for (PropertyDescriptor p : properties) {
                    Method getter = p.getReadMethod();
                    Method setter = p.getWriteMethod();
                    if (getter == null || setter == null) {
                        continue;
                    }
                    OptionMetadata parameterDescription = null;
                    parameterDescription = getter.getAnnotation(OptionMetadata.class);
                    if (parameterDescription == null) {
                        parameterDescription = setter.getAnnotation(OptionMetadata.class);
                    }

                    if (parameterDescription != null && parameterDescription.commandLineParamName().length() > 0) {
                        boolean processOpt = false;
                        String optionValue = "";
                        Object valueToSet = null;
                        if (parameterDescription.commandLineParamIsFlag()) {
                            processOpt = true;
                            valueToSet = (Utils.getFlag(parameterDescription.commandLineParamName(), options));
                        } else {
                            optionValue = Utils.getOption(parameterDescription.commandLineParamName(), options);
                            processOpt = optionValue.length() > 0;
                        }

                        // grab the default/current return value so that we can determine
                        // the type
                        Object value = getter.invoke(target, getterArgs);
                        if (value != null && processOpt) {
                            if (value.getClass().isArray() && ((Object[]) value).length >= 0) {
                                // We're interested in the actual element type...
                                Class<?> elementType = getter.getReturnType().getComponentType();

                                // handle arrays by looking for the option multiple times
                                List<String> optionValues = new ArrayList<String>();
                                optionValues.add(optionValue);
                                while (true) {
                                    optionValue = Utils.getOption(parameterDescription.commandLineParamName(), options);
                                    if (optionValue.length() == 0) {
                                        break;
                                    }
                                    optionValues.add(optionValue);
                                }

                                valueToSet = Array.newInstance(elementType, optionValues.size());
                                for (int i = 0; i < optionValues.size(); i++) {
                                    Object elementObject = null;
                                    if (elementType.isAssignableFrom(File.class)) {
                                        elementObject = new File(optionValues.get(i));
                                    } else {
                                        elementObject = constructOptionHandlerValue(optionValues.get(i));
                                    }
                                    Array.set(valueToSet, i, elementObject);
                                }
                            } else if (value instanceof SelectedTag) {
                                Tag[] legalTags = ((SelectedTag) value).getTags();
                                int tagIndex = Integer.MAX_VALUE;
                                // first try and parse as an integer
                                try {
                                    int specifiedID = Integer.parseInt(optionValue);
                                    for (int z = 0; z < legalTags.length; z++) {
                                        if (legalTags[z].getID() == specifiedID) {
                                            tagIndex = z;
                                            break;
                                        }
                                    }
                                } catch (NumberFormatException e) {
                                    // try to match tag strings
                                    for (int z = 0; z < legalTags.length; z++) {
                                        if (legalTags[z].getReadable().equals(optionValue.trim())) {
                                            tagIndex = z;
                                            break;
                                        }
                                    }
                                }
                                if (tagIndex != Integer.MAX_VALUE) {
                                    valueToSet = new SelectedTag(tagIndex, legalTags);
                                } else {
                                    throw new Exception("Unable to set option: '" + parameterDescription.commandLineParamName() + "'. This option takes a SelectedTag argument, and " + "the supplied value of '" + optionValue + "' " + "does not match any of the legal IDs or strings " + "for it.");
                                }
                            } else if (value instanceof Enum) {
                                Class<?> eClazz = value.getClass();
                                valueToSet = EnumHelper.valueFromString(eClazz, optionValue);
                            } else if (value instanceof OptionHandler) {
                                valueToSet = constructOptionHandlerValue(optionValue);
                            } else if (value instanceof Number) {
                                try {
                                    if (value instanceof Integer) {
                                        valueToSet = new Integer(optionValue);
                                    } else if (value instanceof Long) {
                                        valueToSet = new Long(optionValue);
                                    } else if (value instanceof Double) {
                                        valueToSet = new Double(optionValue);
                                    } else if (value instanceof Float) {
                                        valueToSet = new Float(optionValue);
                                    }
                                } catch (NumberFormatException e) {
                                    throw new Exception("Option: '" + parameterDescription.commandLineParamName() + "' requires a " + value.getClass().getCanonicalName() + " argument");
                                }
                            } else if (value instanceof String) {
                                valueToSet = optionValue;
                            } else if (value instanceof File) {
                                valueToSet = new File(optionValue);
                            }

                            if (valueToSet != null) {
                                // set it!
                                // System.err.println("Setter: " + setter.getName());
                                // System.err.println("Argument: " +
                                // valueToSet.getClass().getCanonicalName());
                                setOption(setter, target, valueToSet);
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }

    /**
     * Construct an instance of an option handler from a String specifying its class
     * name and option values
     *
     * @param optionValue a String containing the class of the option handler
     *                    followed by its options
     * @return an instantiated option handling object
     * @throws Exception if a problem occurs
     */
    protected static Object constructOptionHandlerValue(String optionValue) throws Exception {
        String[] optHandlerSpec = Utils.splitOptions(optionValue);
        if (optHandlerSpec.length == 0) {
            throw new Exception("Invalid option handler specification " + "string '" + optionValue);
        }
        String optionHandler = optHandlerSpec[0];
        optHandlerSpec[0] = "";
        Object handler = Utils.forName(null, optionHandler, optHandlerSpec);

        return handler;
    }

    /**
     * Removes an option from a given list of options.
     *
     * @param list the list to reduce
     * @param name the name of the option
     */
    public static void deleteOption(List<Option> list, String name) {

        for (Iterator<Option> iter = list.listIterator(); iter.hasNext();) {
            Option a = iter.next();
            if (a.name().equals(name)) {
                iter.remove();
            }
        }
    }

    /**
     * Removes an option from a given list of strings that specifies options. This
     * method is for an option that has a parameter value.
     *
     * @param list the list to reduce
     * @param name the name of the option (including hyphen)
     */
    public static void deleteOptionString(List<String> list, String name) {

        for (Iterator<String> iter = list.listIterator(); iter.hasNext();) {
            String a = iter.next();
            if (a.equals(name)) {
                iter.remove();
                iter.next();
                iter.remove();
            }
        }
    }

    /**
     * Removes an option from a given list of strings that specifies options. This
     * method is for an option without a parameter value (i.e., a flag).
     *
     * @param list the list to reduce
     * @param name the name of the option (including hyphen)
     */
    public static void deleteFlagString(List<String> list, String name) {

        for (Iterator<String> iter = list.listIterator(); iter.hasNext();) {
            String a = iter.next();
            if (a.equals(name)) {
                iter.remove();
            }
        }
    }

    /**
     * Set an option value on a target object
     *
     * @param setter     the Method object for the setter method of the option to
     *                   set
     * @param target     the target object on which to set the option
     * @param valueToSet the value of the option to set
     * @throws InvocationTargetException if a problem occurs
     * @throws IllegalAccessException    if a problem occurs
     */
    protected static void setOption(Method setter, Object target, Object valueToSet) throws InvocationTargetException, IllegalAccessException {
        Object[] setterArgs = { valueToSet };
        setter.invoke(target, setterArgs);
    }

    /**
     * Returns the option's description.
     *
     * @return the option's description
     */
    public String description() {

        return m_Description;
    }

    /**
     * Returns the option's name.
     *
     * @return the option's name
     */
    public String name() {

        return m_Name;
    }

    /**
     * Returns the option's number of arguments.
     *
     * @return the option's number of arguments
     */
    public int numArguments() {

        return m_NumArguments;
    }

    /**
     * Returns the option's synopsis.
     *
     * @return the option's synopsis
     */
    public String synopsis() {

        return m_Synopsis;
    }

}
